fix: resolve static/self/parent relative to @param-closure-this bound class#6061
Merged
Merged
Conversation
calebdw
force-pushed
the
calebdw/push-qwswwvrkussm
branch
from
July 18, 2026 03:41
69afe68 to
ea2dddf
Compare
VincentLanglet
approved these changes
Jul 18, 2026
VincentLanglet
left a comment
Contributor
There was a problem hiding this comment.
This seems to fix phpstan/phpstan#11010 ; could you add a non regression test ?
staabm
reviewed
Jul 18, 2026
staabm
reviewed
Jul 18, 2026
staabm
reviewed
Jul 18, 2026
… class When a closure or arrow function has its $this type overridden via @param-closure-this, the static/self/parent keywords in return type declarations were still resolving to the enclosing class instead of the bound class. This also meant the closure scope was not set, preventing access to protected/private members of the bound class. Two fixes were needed: 1. Set inClosureBindScopeClasses when applying @param-closure-this so that resolveTypeByName, resolveName, and canAccessClassMember use the bound class for static/self resolution and visibility checks. 2. Override the InitializerExprContext in getFunctionType when inClosureBindScopeClasses is set, so that static/self/parent in return type declarations (processed via ParserNodeTypeToPHPStanType) also resolve to the bound class instead of the enclosing class. Closes phpstan/phpstan#11010
calebdw
force-pushed
the
calebdw/push-qwswwvrkussm
branch
from
July 20, 2026 14:10
ea2dddf to
e810c66
Compare
Contributor
|
thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
When a closure or arrow function has its
$thistype overridden via@param-closure-this, thestatic/self/parentkeywords in return type declarations resolve to the enclosing class instead of the bound class.Reproducer
Where
Collection::macro()has@param-closure-this static $macro, PHPStan correctly resolves$thisinside the closure toCollection, but thestaticreturn type still resolves toCollectionMacros(the enclosing class) instead ofCollection.Root Cause
Two layers:
inClosureBindScopeClasseswas never set when@param-closure-thiswas applied.NodeScopeResolveronly calledassignVariable('this', ...)to change the$thistype, but did not setinClosureBindScopeClasses— which is whatresolveTypeByNamechecks when resolvingstatic/self/parentkeywords.getFunctionTypeused the enclosing class to resolvestaticin return type declarations. The closure's return typestaticwent throughInitializerExprTypeResolver::getFunctionType→ParserNodeTypeToPHPStanType::resolve, which usedInitializerExprContext::fromScope($this)— always returning the enclosing class via$scope->getClassReflection().Fix
src/Analyser/NodeScopeResolver.phpAfter
assignVariable('this', ...), chain->withClosureBindScopeClasses(...)sostatic/selfresolve to the closure-this class. Applied to both closure and arrow function paths.src/Analyser/MutatingScope.phpwithClosureBindScopeClasses()(new method): Creates a new scope withinClosureBindScopeClassesset, preserving all other state.restoreThis(): RestoreinClosureBindScopeClassesfrom the saved scope (was using$this->, now uses$restoreThisScope->).getFunctionType(): WheninClosureBindScopeClassesis set and the type isstatic/self/parent, pass a context with the bound class toInitializerExprTypeResolverso the return type resolves correctly.Note
Technically it is possible to bind a closure to a newClass and not bind the
$thisinstance:and if we wanted to support this then we would have to create an
@phpstan-closure-scopetag. However, everything that I saw in the wild bound the$newScopeto the same object as the$this; so I opted not to introduce this for the time being. The only time it would really be an issue would be if you wanted a closure to run in the context of a class, but not have a$thisproperty set` (e.g., a static closure)Closes phpstan/phpstan#11010